package com.onluy.frameworkcloud.batis.plugins;


import java.lang.reflect.Method;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.cache.TransactionalCacheManager;
import org.apache.ibatis.executor.BatchExecutor;
import org.apache.ibatis.executor.CachingExecutor;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.ReuseExecutor;
import org.apache.ibatis.executor.SimpleExecutor;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.onluy.frameworkcloud.batis.bean.Batch;
import com.onluy.frameworkcloud.batis.bean.CacheBoundSql;
import com.onluy.frameworkcloud.batis.cache.L2Cache;
import com.onluy.frameworkcloud.batis.holder.BatchHolder;
import com.onluy.frameworkcloud.batis.holder.BoundSqlHolder;
import com.onluy.frameworkcloud.batis.holder.ResultMapHolder;

@Intercepts({@org.apache.ibatis.plugin.Signature(type=Executor.class, method="query", args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @org.apache.ibatis.plugin.Signature(type=Executor.class, method="update", args={MappedStatement.class, Object.class}), @org.apache.ibatis.plugin.Signature(type=StatementHandler.class, method="getBoundSql", args={}), @org.apache.ibatis.plugin.Signature(type=ResultSetHandler.class, method="handleResultSets", args={Statement.class})})
public class WalkbatisPlugin
        implements Interceptor
{
    protected final Logger log = LoggerFactory.getLogger(WalkbatisPlugin.class);

    public final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss";

    public Object plugin(Object target)
    {
        if (((target instanceof Executor)) || ((target instanceof StatementHandler)) || ((target instanceof ResultSetHandler))) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    public Object intercept(Invocation invocation)
            throws Throwable
    {
        long beginTime = System.currentTimeMillis();

        Object target = invocation.getTarget();
        Method method = invocation.getMethod();
        if (("query".equals(method.getName())) && ((target instanceof Executor))) {
            Executor executor = (Executor)target;
            MappedStatement ms = (MappedStatement)invocation.getArgs()[0];
            Object parameter = invocation.getArgs()[1];
            RowBounds rowBounds = (RowBounds)invocation.getArgs()[2];
            ResultHandler resultHandler = (ResultHandler)invocation.getArgs()[3];

            Integer cacheSeconds = null;
            CacheBoundSql cacheBoundSql = BoundSqlHolder.get();
            BoundSql boundSql;
            if ((cacheBoundSql != null) && (cacheBoundSql.getBoundSql() != null)) {
                 boundSql = cacheBoundSql.getBoundSql();
                cacheSeconds = cacheBoundSql.getCacheSeconds();
            } else {
                boundSql = ms.getBoundSql(parameter);
            }

            CacheKey key = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            Object result = null;
            if ((cacheSeconds != null) && (CachingExecutor.class.equals(executor.getClass()))) {
                MetaObject executorMeta = SystemMetaObject.forObject(executor);
                TransactionalCacheManager tcm = (TransactionalCacheManager)executorMeta.getValue("tcm");
                Executor delegate = (Executor)executorMeta.getValue("delegate");

                key.update(Integer.valueOf(cacheSeconds.intValue()));
                result = queryFromCacheByEntity(ms, parameter, rowBounds, resultHandler, key, boundSql, executor, delegate, tcm, cacheSeconds);
            } else {
                result = executor.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }

            printSqlExecuteTime(beginTime, boundSql, parameter, ms.getConfiguration());
            return result;
        }

        if (("update".equals(method.getName())) && (((target instanceof SimpleExecutor)) || ((target instanceof ReuseExecutor)))) {
            MappedStatement ms = (MappedStatement)invocation.getArgs()[0];
            Object parameter = invocation.getArgs()[1];

            CacheBoundSql cacheBoundSql = BoundSqlHolder.get();
            BoundSql boundSql;
            if ((cacheBoundSql != null) && (cacheBoundSql.getBoundSql() != null))
                boundSql = cacheBoundSql.getBoundSql();
            else {
                boundSql = ms.getBoundSql(parameter);
            }

            Integer rows = Integer.valueOf(0);
            Batch batch = BatchHolder.getBatch();
            if ((batch == null) || ((parameter instanceof List))) {
                rows = (Integer)invocation.proceed();
            } else {
                Executor originalExecutor = (Executor)target;
                BatchExecutor batchExecutor = batch.getBatchExecutor();
                if (batch.getBatchExecutor() == null) {
                    batchExecutor = new BatchExecutor(ms.getConfiguration(), originalExecutor.getTransaction());
                    batch.setBatchExecutor(batchExecutor);
                }
                rows = Integer.valueOf(batchExecutor.update(ms, parameter));
                if (batch.getCounter() == 0) {
                    batchExecutor.commit(false);
                }

            }

            printSqlExecuteTime(beginTime, boundSql, parameter, ms.getConfiguration());
            return rows;
        }if ((target instanceof StatementHandler))
    {
        CacheBoundSql cacheBoundSql = BoundSqlHolder.get();
        if ((cacheBoundSql != null) && (cacheBoundSql.getBoundSql() != null))
        {
            BoundSql boundSql = cacheBoundSql.getBoundSql();
            MetaObject shMeta = SystemMetaObject.forObject(target);
            Configuration configuration = (Configuration)shMeta.getValue("delegate.configuration");
            MappedStatement mappedStatement = (MappedStatement)shMeta.getValue("delegate.mappedStatement");
            shMeta.setValue("delegate.boundSql", boundSql);
            shMeta.setValue("delegate.parameterHandler", configuration.newParameterHandler(mappedStatement, boundSql.getParameterObject(), boundSql));
            return boundSql;
        }
        return invocation.proceed();
    }if ((target instanceof ResultSetHandler)) {
        return handleResultSets(invocation);
    }
        return invocation.proceed();
    }

    private <E> List<E> queryFromCacheByEntity(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql, Executor target, Executor delegate, TransactionalCacheManager tcm, Integer cacheSeconds)
            throws Exception
    {
        Cache cache = ms.getCache();
        if (cache != null) {
            invoke(target, "flushCacheIfRequired", new Object[] { ms }, new Class[] { ms.getClass() });
            if (resultHandler == null) {
                invoke(target, "ensureNoOutParams", new Object[] { ms, parameterObject, boundSql }, new Class[] { ms.getClass(), Object.class, boundSql.getClass() });
                List list = (List)tcm.getObject(cache, key);
                if (list == null) {
                    list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    tcm.putObject(cache, key, list);

                    target.commit(true);

                    if (cache.getClass().equals(L2Cache.class)) {
                        ((L2Cache)cache).expire(key, cacheSeconds.longValue());
                    }
                }
                return list;
            }
        }
        return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

    private Object handleResultSets(Invocation invocation)
            throws Throwable
    {
        Object target = invocation.getTarget();
        MetaObject resultSetHandlerMeta = SystemMetaObject.forObject(target);
        Configuration configuration = (Configuration)resultSetHandlerMeta.getValue("configuration");
        Map nextResultMaps = (Map)resultSetHandlerMeta.getValue("nextResultMaps");
        MappedStatement mappedStatement = (MappedStatement)resultSetHandlerMeta.getValue("mappedStatement");

        ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
        Statement stmt = (Statement)invocation.getArgs()[0];
        List multipleResults = new ArrayList();
        int resultSetCount = 0;
        Object rsw = invoke(target, "getFirstResultSet", new Object[] { stmt }, new Class[] { Statement.class });

        List resultMaps = ResultMapHolder.get();
        if (resultMaps == null) {
            resultMaps = mappedStatement.getResultMaps();
        }
        int resultMapCount = resultMaps.size();
        invoke(target, "validateResultMapsCount", new Object[] { rsw, Integer.valueOf(resultMapCount) }, new Class[] { rsw.getClass(), Integer.TYPE });
        while ((rsw != null) && (resultMapCount > resultSetCount)) {
            ResultMap resultMap = (ResultMap)resultMaps.get(resultSetCount);
            invoke(target, "handleResultSet", new Object[] { rsw, resultMap, multipleResults, null }, new Class[] { rsw.getClass(), ResultMap.class, List.class, ResultMapping.class });
            rsw = invoke(target, "getNextResultSet", new Object[] { stmt }, new Class[] { Statement.class });
            invoke(target, "cleanUpAfterHandlingResultSet", null, null);
            resultSetCount++;
        }

        String[] resultSets = mappedStatement.getResulSets();
        if (resultSets != null) {
            while ((rsw != null) && (resultSetCount < resultSets.length)) {
                ResultMapping parentMapping = (ResultMapping)nextResultMaps.get(resultSets[resultSetCount]);
                if (parentMapping != null) {
                    String nestedResultMapId = parentMapping.getNestedResultMapId();
                    ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
                    invoke(target, "handleResultSet", new Object[] { rsw, resultMap, null, parentMapping }, new Class[] { rsw.getClass(), ResultMap.class, List.class, ResultMapping.class });
                }
                rsw = invoke(target, "getNextResultSet", new Object[] { stmt }, new Class[] { Statement.class });
                invoke(target, "cleanUpAfterHandlingResultSet", null, null);
                resultSetCount++;
            }
        }
        return multipleResults.size() == 1 ? (List)multipleResults.get(0) : multipleResults;
    }

    public void setProperties(Properties properties)
    {
    }

    private static Object invoke(Object target, String methodName, Object[] params, Class<?>[] paramsClass)
            throws Exception
    {
        Method method = target.getClass().getDeclaredMethod(methodName, paramsClass);
        method.setAccessible(true);
        return method.invoke(target, params);
    }

    private void printSqlExecuteTime(long beginTime, BoundSql boundSql, Object parameterObject, Configuration configuration)
    {
        if (this.log.isDebugEnabled()) {
            String sql = getSql(boundSql, parameterObject, configuration);
            this.log.debug("SQL execute time: {}s - SQL: {}", Double.valueOf((System.currentTimeMillis() - beginTime) / 1000.0D), sql);
        }
    }

    private String getSql(BoundSql boundSql, Object parameterObject, Configuration configuration)
    {
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        try {
            List parameterMappings = boundSql.getParameterMappings();
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (parameterMappings != null)
                for (int i = 0; i < parameterMappings.size(); i++) {
                    ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
                    if (parameterMapping.getMode() == ParameterMode.OUT)
                        continue;
                    String propertyName = parameterMapping.getProperty();
                    Object value;
                    if (boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    }
                    else
                    {
                        if (parameterObject == null) {
                            value = null;
                        }
                        else
                        {
                            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                                value = parameterObject;
                            } else {
                                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                                value = metaObject.getValue(propertyName);
                            }
                        }
                    }
                    sql = replacePlaceholder(sql, value);
                }
        }
        catch (Exception e)
        {
            this.log.error(e.getMessage(), e);
        }
        return sql;
    }

    private String replacePlaceholder(String sql, Object propertyValue)
    {
        String result;
        if (propertyValue != null)
        {
            if ((propertyValue instanceof String)) {
                result = "'" + propertyValue + "'";
            }
            else
            {
                if ((propertyValue instanceof Date))
                    result = "'" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(propertyValue) + "'";
                else
                    result = propertyValue.toString();
            }
        } else {
            result = "null";
        }
        return sql.replaceFirst("\\?", result);
    }
}